/*
 * Copyright 2014 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.netflix.servo.monitor;

import com.netflix.servo.util.UnmodifiableList;
import com.netflix.servo.tag.BasicTagList;
import com.netflix.servo.tag.SmallTagMap;
import com.netflix.servo.tag.Tag;
import com.netflix.servo.tag.TagList;
import com.netflix.servo.tag.Tags;
import com.netflix.servo.util.Preconditions;

import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Configuration settings associated with a monitor. A config consists of a name that is required
 * and an optional set of tags.
 */
public final class MonitorConfig {

    /**
     * A builder to assist in creating monitor config objects.
     */
    public static class Builder {
        private final String name;
        private SmallTagMap.Builder tagsBuilder = SmallTagMap.builder();
        private PublishingPolicy policy = DefaultPublishingPolicy.getInstance();

        /**
         * Create a new builder initialized with the specified config.
         */
        public Builder(MonitorConfig config) {
            this(config.getName());
            withTags(config.getTags());
            withPublishingPolicy(config.getPublishingPolicy());
        }

        /**
         * Create a new builder initialized with the specified name.
         */
        public Builder(String name) {
            this.name = name;
        }

        /**
         * Add a tag to the config.
         */
        public Builder withTag(String key, String val) {
            tagsBuilder.add(Tags.newTag(key, val));
            return this;
        }

        /**
         * Add a tag to the config.
         */
        public Builder withTag(Tag tag) {
            tagsBuilder.add(tag);
            return this;
        }

        /**
         * Add all tags in the list to the config.
         */
        public Builder withTags(TagList tagList) {
            if (tagList != null) {
                for (Tag t : tagList) {
                    tagsBuilder.add(t);
                }
            }
            return this;
        }

        /**
         * Add all tags in the list to the config.
         */
        public Builder withTags(Collection<Tag> tagCollection) {
            tagsBuilder.addAll(tagCollection);
            return this;
        }

        /**
         * Add all tags from a given SmallTagMap.
         */
        public Builder withTags(SmallTagMap.Builder tagsBuilder) {
            this.tagsBuilder = tagsBuilder;
            return this;
        }

        /**
         * Add the publishing policy to the config.
         */
        public Builder withPublishingPolicy(PublishingPolicy policy) {
            this.policy = policy;
            return this;
        }

        /**
         * Create the monitor config object.
         */
        public MonitorConfig build() {
            return new MonitorConfig(this);
        }

        /**
         * Get the name for this monitor config.
         */
        public String getName() {
            return name;
        }

        /**
         * Get the list of tags for this monitor config.
         */
        public List<Tag> getTags() {
            return UnmodifiableList.copyOf(tagsBuilder.result());
        }

        /**
         * Get the publishingPolicy.
         */
        public PublishingPolicy getPublishingPolicy() {
            return policy;
        }
    }

    /**
     * Return a builder instance with the specified name.
     */
    public static Builder builder(String name) {
        return new Builder(name);
    }

    private final String name;
    private final TagList tags;
    private final PublishingPolicy policy;

    /**
     * Config is immutable, cache the hash code to improve performance.
     */
    private final AtomicInteger cachedHashCode = new AtomicInteger(0);

    /**
     * Creates a new instance with a given name and tags. If {@code tags} is
     * null an empty tag list will be used.
     */
    private MonitorConfig(Builder builder) {
        this.name = Preconditions.checkNotNull(builder.name, "name");
        this.tags = (builder.tagsBuilder.isEmpty())
                ? BasicTagList.EMPTY
                : new BasicTagList(builder.tagsBuilder.result());
        this.policy = builder.policy;
    }

    /**
     * Returns the name of the metric.
     */
    public String getName() {
        return name;
    }

    /**
     * Returns the tags associated with the metric.
     */
    public TagList getTags() {
        return tags;
    }

    /**
     * Returns the publishing policy.
     */
    public PublishingPolicy getPublishingPolicy() {
        return policy;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || !(obj instanceof MonitorConfig)) {
            return false;
        }
        MonitorConfig m = (MonitorConfig) obj;
        return name.equals(m.getName())
                && tags.equals(m.getTags())
                && policy.equals(m.getPublishingPolicy());
    }

    /**
     * This class is immutable so we cache the hash code after the first time it is computed. The
     * value 0 is used as an indicator that the hash code has not yet been computed, this means the
     * cache won't work for a small set of inputs, but the impact should be minimal for a decent
     * hash function. Similar technique is used for java String class.
     */
    @Override
    public int hashCode() {
        int hash = cachedHashCode.get();
        if (hash == 0) {
            hash = name.hashCode();
            hash = 31 * hash + tags.hashCode();
            hash = 31 * hash + policy.hashCode();
            cachedHashCode.set(hash);
        }
        return hash;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "MonitorConfig{name=" + name + ", tags=" + tags + ", policy=" + policy + '}';
    }

    /**
     * Returns a copy of the current MonitorConfig.
     */
    private Builder copy() {
        return MonitorConfig.builder(name).withTags(tags).withPublishingPolicy(policy);
    }

    /**
     * Returns a copy of the monitor config with an additional tag.
     */
    public MonitorConfig withAdditionalTag(Tag tag) {
        return copy().withTag(tag).build();
    }

    /**
     * Returns a copy of the monitor config with additional tags.
     */
    public MonitorConfig withAdditionalTags(TagList newTags) {
        return copy().withTags(newTags).build();
    }
}
